package org.dei.perla.sys.configuration;

import java.io.File;
import java.util.ArrayList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.dei.perla.component.Component;
import org.dei.perla.sys.device.adapter.Gateway;
import org.dei.perla.sys.device.channel.Channel;
import org.dei.perla.sys.device.channel.ChannelManager;
import org.dei.perla.sys.device.channel.concrete.serial.SerialChannel;
import org.dei.perla.sys.device.channel.concrete.serial.SerialChannelConfiguration;
import org.dei.perla.sys.device.channel.concrete.serial.SerialChannelManager;
import org.dei.perla.sys.device.channel.concrete.socket.SocketChannel;
import org.dei.perla.sys.device.channel.concrete.socket.SocketChannelConfiguration;
import org.dei.perla.sys.device.channel.concrete.socket.SocketChannelManager;
import org.dei.perla.sys.device.channel.frame.FrameConfiguration;
import org.dei.perla.sys.device.channel.frame.address.FrameAddress;
import org.dei.perla.sys.device.channel.frame.address.concrete.SerialFrameAddress;
import org.dei.perla.sys.device.channel.frame.address.concrete.SimpleFrameAddress;
import org.dei.perla.sys.device.channel.frame.address.concrete.SocketFrameAddress;
import org.dei.perla.sys.device.channel.frame.header.FrameHeader;
import org.dei.perla.sys.device.channel.frame.header.FrameHeaderReceiver;
import org.dei.perla.sys.device.channel.frame.header.FrameHeaderSender;
import org.dei.perla.sys.device.channel.frame.header.concrete.simple.SimpleFrameHeader;
import org.dei.perla.sys.device.channel.frame.header.concrete.simple.SimpleFrameHeaderReceiver;
import org.dei.perla.sys.device.channel.frame.header.concrete.simple.SimpleFrameHeaderSender;
import org.dei.perla.sys.device.channel.frame.receiver.FrameReceiverHeaderLess;
import org.dei.perla.sys.device.channel.frame.sender.FrameSenderHeaderLess;
import org.dei.perla.utils.logger.LogRecord.Type;
import org.dei.perla.utils.logger.LogRecord.Verbosity;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

/**
 * Questo componente si occupa di leggere la configurazione del sistema da un file XML
 * e inizializzare tutti gli altri componenti del sistema di conseguenza.
 * E' molto importante costruire un file DTD associato al file XML in modo da validarlo.
 * Il controllo degli errori è basato interamente sulla validazione del file XML.
 *
 * @todo creare DTD e abilitare controllo DTD in pParseConfiguration()
 */
public class ConfigurationManager extends Component {

	/**
	 * Tag XML di configurazione
	 */
	private final static String TAG_PARAM								= "param";
	private final static String TAG_PARAM_NAME							= "name";
	
	private final static String TAG_CHANNEL_MANAGER						= "channel-manager";
	private final static String TAG_CHANNEL_MANAGER_ID					= "channel-manager-id";
	private final static String TAG_CHANNEL_MANAGER_NAME				= "channel-manager-name";
	private final static String TAG_CHANNEL_MANAGER_TYPE				= "channel-manager-type";
	private final static String TAG_CHANNEL_MANAGER_CONFIG				= "channel-manager-config";
	
	private final static String TAG_CHANNEL 							= "channel";
	private final static String TAG_CHANNEL_NAME 						= "channel-name";
	private static final String TAG_CHANNEL_CONFIG 						= "channel-config"
		;
	private static final String TAG_CHANNEL_FRAME_CONFIG 				= "frame-config";
	private static final String TAG_CHANNEL_FRAME_HEADER_CONFIG			= "frame-header-config";
	private static final String TAG_CHANNEL_FRAME_HEADER_TYPE 			= "frame-header-type";
	private static final String TAG_CHANNEL_FRAME_HEADER_ADDRESS 		= "frame-local-address";
	
	private final static String TAG_GATEWAY 							= "gateway";
	private final static String TAG_GATEWAY_ROUTE_BINDING 				= "route-binding";
	private final static String TAG_GATEWAY_ROUTE_BINDING_SRC			= "route-binding-src";
	private final static String TAG_GATEWAY_ROUTE_BINDING_DST			= "route-binding-dst";
	private final static String TAG_GATEWAY_ROUTE_BINDING_DST_ADDRESS	= "route-binding-dst-address";
	
	/**
	 * Tipi "enumerativi" nell'XML predefinito
	 */
	private static final String TYPE_FRAME_HEADER_SIMPLE = "simple";
	private static final String TYPE_CHANNEL_SOCKET 	= "socket";
	private static final String TYPE_CHANNEL_SERIAL 	= "serial";
	
	/**
	 * Path al file XML di configurazione
	 */
	private String pConfigFilePath;
	
	/**
	 * Radice DOM del documento XML letto
	 */
	private Document pDoc;
	
	/**
	 * Canali istanziati
	 */
	private ArrayList<ChannelManager> pChannels;
	
	/**
	 * Gateway inizializzato
	 */
	private Gateway pGateway;
	
	/**
	 * Costruttore, Inizializza il manager della configurazione di sistema
	 * @param parConfigFile Path al file XML contenente la configurazione
	 */ 
	public ConfigurationManager(String parConfigFile) {
		super("ConfigurationManager");
		this.pConfigFilePath 	= parConfigFile;
		this.pChannels 			= new ArrayList<ChannelManager>();
		this.pGateway			= null;
	}
	
	/**
	 * Avvia la configurazione del sistema
	 */
	public void start() {
		super.start();
		if (this.pParseConfiguration()) {
			this.pParseChannelConfig();
			this.pParseGatewayConfig();
		} else {
			String tMessage = "Errore irreversibile. Impossibile inizializzare il sistema.";
			loggerNotify(Type.Error, tMessage, Verbosity.Low);
		}
	}
		
	/**
	 * Restituisce tutti i manager di canale inizializzati
	 * @return
	 */
	public ArrayList<ChannelManager> getChannels() {
		return this.pChannels;
	}
	
	/**
	 * Restituisce il Gateway se il nodo ne ha uno, altrimenti restituisce NULL.
	 * @return
	 */
	public Gateway getGateway() {
		return this.pGateway;
	}
	
	/**
	 * Parsa il file XML di configurazione
	 * @return Restituisce true se il file di configurazione è stato letto e parsato correttamente, false altrimenti
	 */
	private boolean pParseConfiguration() {
		
		// Inizializza il parser DOM
		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();	
		dbf.setNamespaceAware(true);
        //dbf.setValidating(true);
        dbf.setIgnoringElementContentWhitespace(true);
        dbf.setIgnoringComments(true);

		try {

			DocumentBuilder db = dbf.newDocumentBuilder();
			this.pDoc = db.parse(new File(this.pConfigFilePath));
			return true;
			
		} catch (Exception e) {
			
			String tMessage = "Errore durante la lettura del file XML di configurazione (" + e.getMessage() + ")";
			loggerNotify(Type.Error, tMessage, Verbosity.Low);
			return false;
		
		} 
	
	}
	
	/**
	 * Legge la configurazione dei canali
	 *
	 */
	private void pParseChannelConfig() {
		
		// Leggi tutte le configurazioni dei vari canali
		NodeList tChannelManager = this.pDoc.getElementsByTagName(ConfigurationManager.TAG_CHANNEL_MANAGER);
		
		for (int j = 0; j < tChannelManager.getLength(); j++) {
			
			// Estrai un channel manager
			Element tCurChannelManager = (Element) tChannelManager.item(j);
			
			// Leggi il nome del canale
			int tChannelManagerId = new Integer(tCurChannelManager.getElementsByTagName(ConfigurationManager.TAG_CHANNEL_MANAGER_ID).item(0).getTextContent()).intValue();
			
			// Leggi il nome del canale
			String tChannelManagerName = tCurChannelManager.getElementsByTagName(ConfigurationManager.TAG_CHANNEL_MANAGER_NAME).item(0).getTextContent();
			
			// Leggi il tipo del canale
			String tChannelManagerType = tCurChannelManager.getElementsByTagName(ConfigurationManager.TAG_CHANNEL_MANAGER_TYPE).item(0).getTextContent();

			// Leggi i parametri di configurazione del channel manager
			Element tChannelManagerConfigTag = (Element) tCurChannelManager.getElementsByTagName(ConfigurationManager.TAG_CHANNEL_MANAGER_CONFIG).item(0);
			ArrayList<ConfigurationParameter> tChannelManagerConfig = pExtractParameter(tChannelManagerConfigTag);
			
			// Leggi la configurazione del frame
			ArrayList<ConfigurationParameter> tChannelFrameConfiguration = this.pExtractParameter((Element) tCurChannelManager.getElementsByTagName(ConfigurationManager.TAG_CHANNEL_FRAME_CONFIG).item(0));
			FrameConfiguration tChannelManagerFrameConfig = null;
			
			// Leggi la configurazione dell'header del frame
			boolean tChannelHasHeader = false;
			FrameHeaderConfig tChannelFrameHeaderConfig = null;
			NodeList tChannelFrameHeaderConfigLst = tCurChannelManager.getElementsByTagName(ConfigurationManager.TAG_CHANNEL_FRAME_HEADER_CONFIG);
			if ( tChannelFrameHeaderConfigLst.getLength() > 0 ) {
				
				tChannelHasHeader = true;
				String tChannelFrameHeaderType 		= null;
				String tChannelFrameLocalAddressStr = null;
				
				// Leggi i parametri di configurazione
				NodeList tParams = ((Element) tChannelFrameHeaderConfigLst.item(0)).getElementsByTagName(ConfigurationManager.TAG_PARAM);
				for (int k = 0; k < tParams.getLength(); k++) {
				
					Attr tempAttr = ((Element) tParams.item(k)).getAttributeNode(ConfigurationManager.TAG_PARAM_NAME);
					if ( tempAttr.getValue().equalsIgnoreCase(ConfigurationManager.TAG_CHANNEL_FRAME_HEADER_TYPE)) {
						tChannelFrameHeaderType = tempAttr.getOwnerElement().getTextContent();
					} else if ( tempAttr.getValue().equalsIgnoreCase(ConfigurationManager.TAG_CHANNEL_FRAME_HEADER_ADDRESS)) {
						tChannelFrameLocalAddressStr = tempAttr.getOwnerElement().getTextContent();
					} 
					
				}
				
				tChannelFrameHeaderConfig = this.pSelectFrameHeader(tChannelFrameHeaderType, tChannelFrameLocalAddressStr);

			}
			
			FrameHeader tChannelFrameHeader = null;
			if (tChannelHasHeader) {
				tChannelFrameHeader = tChannelFrameHeaderConfig.getFrameHeader();
			}
			
			tChannelManagerFrameConfig = new FrameConfiguration(tChannelFrameConfiguration, tChannelFrameHeader);
			
			// Leggi tutte le configurazioni dei vari canali
			NodeList tChannels = tCurChannelManager.getElementsByTagName(ConfigurationManager.TAG_CHANNEL);

			ArrayList<Channel> tInstancedChannels = new ArrayList<Channel>();
			
			// Per ogni canale leggine la configurazione
			for (int i = 0; i < tChannels.getLength(); i++) {
				
				// Estrai un canale
				Element tChannel = (Element) tChannels.item(i);
				
				// Leggi il nome del canale
				String tChannelName = tChannel.getElementsByTagName(ConfigurationManager.TAG_CHANNEL_NAME).item(0).getTextContent();
				
				// Leggi la configurazione del canale e istanzialo
				ArrayList<ConfigurationParameter> tChannelConfig = this.pExtractParameter((Element) tChannel.getElementsByTagName(ConfigurationManager.TAG_CHANNEL_CONFIG).item(0));
				
				// Istanzia un canale socket
				if (tChannelManagerType.equalsIgnoreCase(ConfigurationManager.TYPE_CHANNEL_SOCKET)) {
				
					SocketChannelConfiguration tChannelSocketConfig = new SocketChannelConfiguration(tChannelConfig);

					tInstancedChannels.add(new SocketChannel(tChannelName, new FrameReceiverHeaderLess(tChannelManagerFrameConfig), new FrameSenderHeaderLess(tChannelManagerFrameConfig), tChannelSocketConfig));
					
				// Istanzia un canale seriale
				}  else if (tChannelManagerType.equalsIgnoreCase(ConfigurationManager.TYPE_CHANNEL_SERIAL)) {
					
					SerialChannelConfiguration tChannelSerialConfig = new SerialChannelConfiguration(tChannelConfig);
					
					tInstancedChannels.add(new SerialChannel(tChannelName, new FrameReceiverHeaderLess(tChannelManagerFrameConfig), new FrameSenderHeaderLess(tChannelManagerFrameConfig), tChannelSerialConfig));
	
				}
			}
			
			// Istanzia un channel manager socket
			if (tChannelManagerType.equalsIgnoreCase(ConfigurationManager.TYPE_CHANNEL_SOCKET)) {
				
				this.pChannels.add(new SocketChannelManager(tChannelManagerId, tChannelManagerName, tInstancedChannels, 
						tChannelManagerFrameConfig, tChannelManagerConfig));
				
			// Istanzia un channel manager seriale	
			}  else if (tChannelManagerType.equalsIgnoreCase(ConfigurationManager.TYPE_CHANNEL_SERIAL)) {
				this.pChannels.add(new SerialChannelManager(tChannelManagerId, tChannelManagerName, tInstancedChannels, tChannelManagerFrameConfig));
			}
			
		}

		
	}
	
	/**
	 * Leggi la configurazione del gateway
	 *
	 */
	private void pParseGatewayConfig() {
		
		// Leggi la configurazione del gateway
		Element tGateway = (Element) this.pDoc.getElementsByTagName(ConfigurationManager.TAG_GATEWAY).item(0);

		// Il nodo non dispone di un gateway
		if ( tGateway == null ) {
			
			return;
		
		} else {
		
			NodeList tRoutes = tGateway.getElementsByTagName(ConfigurationManager.TAG_GATEWAY_ROUTE_BINDING);
			
			ArrayList<Integer> tRoutesBindingSrc = new ArrayList<Integer>();
			ArrayList<Integer> tRoutesBindingDst = new ArrayList<Integer>();
			ArrayList<String> tRoutesBindingDstAddressStr = new ArrayList<String>();
			
			// Per ogni rotta di routing leggine la configurazione
			for (int i = 0; i < tRoutes.getLength(); i++) {
				
				// Estrae una rotta
				Element tRoute = (Element) tRoutes.item(i);
			
				// Leggi l'identificativo del channel manager sorgente
				tRoutesBindingSrc.add(new Integer(tRoute.getElementsByTagName(ConfigurationManager.TAG_GATEWAY_ROUTE_BINDING_SRC).item(0).getTextContent()));
				
				// Leggi l'identificativo del channel manager destinazione
				tRoutesBindingDst.add(new Integer(tRoute.getElementsByTagName(ConfigurationManager.TAG_GATEWAY_ROUTE_BINDING_DST).item(0).getTextContent()));
				
				// Leggi l'indirizzo del nodo destinazione
				tRoutesBindingDstAddressStr.add(tRoute.getElementsByTagName(ConfigurationManager.TAG_GATEWAY_ROUTE_BINDING_DST_ADDRESS).item(0).getTextContent());
				
			}
			
			// Copia l'array list in due array statici
			int [] tSrcArray = new int[tRoutesBindingSrc.size()];
			int [] tDstArray = new int[tRoutesBindingDst.size()];
			FrameAddress[] tDstAddressArray = new FrameAddress[tRoutesBindingSrc.size()];
			
			for (int i = 0; i < tRoutesBindingSrc.size(); i++) {
				tSrcArray[i] = tRoutesBindingSrc.get(i);
				tDstArray[i] = tRoutesBindingDst.get(i);
			}
			
			// Crea l'oggetto FrameAddress corretto per indirizzo letto
			for (int i = 0; i < tRoutesBindingDstAddressStr.size(); i++) {
				
				// Cerca il channel manager di destinazione
				for (int j = 0; j < this.pChannels.size(); j++) {
					
					ChannelManager tTempChannelMgr = this.pChannels.get(j);
					
					if ( tTempChannelMgr.getChannelId() == tDstArray[i] ) {
						
						if ( tTempChannelMgr instanceof SocketChannelManager ) {
							tDstAddressArray[i] =  new SocketFrameAddress(tRoutesBindingDstAddressStr.get(i));
						} else if ( tTempChannelMgr instanceof SerialChannelManager ) {
							tDstAddressArray[i] =  new SerialFrameAddress(tRoutesBindingDstAddressStr.get(i));
						}
						
					}
				}
			}
			
			this.pGateway = new Gateway(this.pChannels);
			this.pGateway.setStaticRoutes(tSrcArray, tDstArray, tDstAddressArray);
			
		}
		
	}
	
	/**
	 * Seleziona il tipo di header e gli oggetti per ricevere/spedire l'header correttamente
	 * in base alla stringa passata come parametro.
	 * @param parStr Tipo di header
	 * @return Restituisce la configurazione dell'header
	 */
	private FrameHeaderConfig pSelectFrameHeader(String parStr, String parLocalAddressStr) {
		
		FrameHeaderConfig tFrameConfig 	= null;
		FrameAddress tFrameLocalAddress = null;
		if ( parStr.equalsIgnoreCase(ConfigurationManager.TYPE_FRAME_HEADER_SIMPLE)) {
			
			tFrameLocalAddress = new SimpleFrameAddress();
			tFrameLocalAddress.setAddressFromString(parLocalAddressStr);
			
			tFrameConfig = new FrameHeaderConfig(new SimpleFrameHeader(), 
					new SimpleFrameHeaderReceiver(), new SimpleFrameHeaderSender(), tFrameLocalAddress);
		}
		
		return tFrameConfig;
		
	}
	
	/**
	 * Restituisce una lista di parametri estratti da un elemento
	 * @param parElement L'elemento contenente i parametri
	 * @return
	 */
	private ArrayList<ConfigurationParameter> pExtractParameter(Element parElement) {
		
		ArrayList<ConfigurationParameter> tTemp = new ArrayList<ConfigurationParameter>();
		
		NodeList tNodes = parElement.getElementsByTagName(ConfigurationManager.TAG_PARAM);
		for (int i = 0; i < tNodes.getLength(); i++) {
			String tParName = ((Element) tNodes.item(i)).getAttribute(TAG_PARAM_NAME);
			String tParValue = ((Element) tNodes.item(i)).getTextContent();
			tTemp.add(new ConfigurationParameter(tParName, tParValue));
		}
		
		return tTemp;
		
	}
	
	/**
	 * Gestisce il passaggio di informazioni semplificato tra il metodo
	 * pSelectFrameHeader e il resto della classe padre
	 *
	 */
	private class FrameHeaderConfig {
		
		private FrameHeader pFrameHeader;
		private FrameHeaderReceiver pFrameHeaderReceiver;
		private FrameHeaderSender pFrameHeaderSender;
		private FrameAddress pFrameLocalAddress;
		
		public FrameHeaderConfig(FrameHeader parFrameHeader, FrameHeaderReceiver parFrameHeaderReceiver, 
				FrameHeaderSender parFrameHeaderSender, FrameAddress parFrameLocalAddress) {
			super();
			pFrameHeader 			= parFrameHeader;
			pFrameHeaderReceiver 	= parFrameHeaderReceiver;
			pFrameHeaderSender 		= parFrameHeaderSender;
			pFrameLocalAddress		= parFrameLocalAddress;
		}
		
		public FrameHeader getFrameHeader() {
			return pFrameHeader;
		}
		
		public FrameHeaderReceiver getFrameHeaderReceiver() {
			return pFrameHeaderReceiver;
		}
		
		public FrameHeaderSender getFrameHeaderSender() {
			return pFrameHeaderSender;
		}
		
		public FrameAddress getFrameLocalAddress() {
			return pFrameLocalAddress;
		}
		
	}
	
}

